/*
 * Copyright (C) 2010 ST-Ericsson AS
 * Author: Erwan Bracq / erwan.bracq@stericsson.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 */

#include <stdio.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>

#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <dbus/dbus.h>

#define UNUSED(expr) do { (void)(expr); } while (0)

#ifdef HAVE_ANDROID_OS
static const char* RUNNING_DIR =  "/data/ste";
static const char* PID_FILE_NAME = "/data/ste/lifecycle.pid";
static const char* LOG_FILE_NAME = "/data/ste/lifecycle.log";
#else
static const char* RUNNING_DIR =  "/lib/firmware/ste/modem";
static const char* PID_FILE_NAME = "/var/run/lifecycle.pid";
static const char* LOG_FILE_NAME = "/var/log/lifecycle.log";
#endif

static bool g_should_reopen_log = false;
static FILE* g_log = NULL;

/* MID signal message handler */
static DBusHandlerResult handle_dbus_message(DBusConnection *dbcon, DBusMessage *msg, void *data) {
	DBusError err;
	char* state;
	char* reason;
	const time_t now=time(0);
	char now_str[100];

	UNUSED(dbcon);
	UNUSED(data);
	/* Do this first, so that we can rely on being able to use g_log for
	 * writing error messages. We're not allowed to write a single byte
	 * to the old file after the sighup, as that would ruin log rotation
	 * with compression.
	 */
	if (g_should_reopen_log) {
		if (g_log != NULL)
			fclose(g_log);

		g_log = fopen(LOG_FILE_NAME, "at");
		if (g_log != NULL) {
			setlinebuf(g_log);
			g_should_reopen_log = false;
		}
		/* Nowhere to log the error */
	}

	dbus_error_init(&err);

	if (!dbus_message_is_signal(msg, "com.stericsson.mid.Modem", "StateChange")) {
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	if (!dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &state, DBUS_TYPE_STRING, &reason, DBUS_TYPE_INVALID)) {
		if (g_log)
			fprintf(g_log, "Failed to read StatusChange arguments: %s\n", err.message);
		dbus_error_free(&err);
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	strftime(now_str, sizeof(now_str), "%Y-%m-%dT%H:%M:%S", localtime(&now));

	if (g_log != NULL)
		fprintf(g_log, "%s: %s (%s)\n", now_str, state, reason);

	return DBUS_HANDLER_RESULT_HANDLED;
}

static void handle_signal(int signal)
{
	switch (signal) {
	case SIGHUP:
		g_should_reopen_log = true;
		break;
	}
}

#ifdef HAVE_ANDROID_OS
static bool write_pid_file(const char* pidfile __attribute__((unused)))
{
	return true;
}
#else
static bool write_pid_file(const char* pidfile)
{
	int fd = open(pidfile, O_WRONLY|O_CREAT, 0640);
	FILE* f = NULL;
	bool success = false;

	if (fd < 0) {
		fprintf(stderr, "Failed to open PID file %s for writing: %s\n", pidfile, strerror(errno));
		goto exit;
	}
	if (lockf(fd, F_TLOCK, 0) < 0) {
		fprintf(stderr, "PID file lock failure: %s\n", strerror(errno));
		goto exit;
	}
	f = fdopen(fd, "wt");
	if (f == NULL) {
		fprintf(stderr, "PID file lock failure: %s\n", strerror(errno));
		goto exit;
	}
	/* Record pid to file */
	fprintf(f, "%d\n", getpid());
	success = true;
exit:
	if (f != NULL) {
		fclose(f);
	} else if (fd != -1) {
		close(fd);
	}
	return success;
}
#endif

static bool daemonize_sub(const char *rdir, const char *pidfile)
{
	pid_t sid;

	/* Change the file permissions */
	umask(027);

	/* Change to running directory */
	if (chdir(rdir) < 0) {
		fprintf(stderr, "Failed to change to running dir %s: %s\n", rdir, strerror(errno));
		goto error;
	}

	if (pidfile) {
		write_pid_file(pidfile);
	}

	/* Create a new SID for the child process */
	sid = setsid();
	if (sid < 0) {
		fprintf(stderr, "Unabled to get SID for child process: %s\n", strerror(errno));
		goto error;
	}

	/* Ignore TTY */
	sigaction(SIGTSTP, &(struct sigaction){.sa_handler=SIG_IGN}, 0);
	sigaction(SIGTTOU, &(struct sigaction){.sa_handler=SIG_IGN}, 0);
	sigaction(SIGTTIN, &(struct sigaction){.sa_handler=SIG_IGN}, 0);
	/* Catch hangup */
	if (sigaction(SIGHUP, &(struct sigaction){.sa_handler=handle_signal}, 0) == -1) {
		fprintf(stderr, "Failed to trap SIGHUP: %s\n", strerror(errno));
		goto error;
	}


	close(STDIN_FILENO);
	close(STDOUT_FILENO);
	close(STDERR_FILENO);

	freopen("/dev/null", "r", stdin);
	freopen("/dev/null", "w", stdout);
	freopen("/dev/null", "w", stderr);


	return true;
error:
	/* TODO: Clean up */
	return false;
}

static bool daemonize(const char *rdir, const char *pidfile)
{
	pid_t pid;
	/* Already a daemon */
	if (getppid() == 1) {
		fprintf(stderr, "Already a daemon\n");

		return daemonize_sub(rdir, pidfile);
	}

	/* Fork process */
	pid = fork();
	if (pid < 0) {
		fprintf(stderr, "Fork failure: %s\n", strerror(errno));
		goto error;
	}
	if (pid > 0) {
		exit(0);
	}

	return daemonize_sub(rdir, pidfile);
error:
	/* TODO: Clean up */
	return false;
}

int main(void) {
	DBusError err;
	DBusConnection *dbcon;

	dbus_error_init(&err);
	dbcon = dbus_bus_get(DBUS_BUS_SYSTEM, &err);
	if(!dbcon || dbus_error_is_set(&err)) {
		fprintf(stderr, "DBUS get error %s: %s\n", err.name, err.message);
		goto error;
	}

	dbus_bus_add_match(dbcon,
		"type='signal', interface='com.stericsson.mid.Modem'", &err);
	if(dbus_error_is_set(&err)) {
		fprintf(stderr, "DBUS match error %s: %s\n", err.name, err.message);
		goto error;
	}

	if(!dbus_connection_add_filter(dbcon,
		handle_dbus_message, stdout, NULL)) {
		fprintf(stderr, "DBUS filter error\n");
		goto error;
	};

	g_log = fopen(LOG_FILE_NAME, "at");
	if (g_log == NULL) {
		fprintf(stderr, "Failed to open log file \"%s\" for appending: %s\n", LOG_FILE_NAME, strerror(errno));
		goto error;
	}
	setlinebuf(g_log);

	/* Daemonize last, so that errors are printed before spawn */
	if(!daemonize(RUNNING_DIR, PID_FILE_NAME))
		goto error;

	while(dbus_connection_read_write_dispatch(dbcon, -1));

	dbus_bus_remove_match (dbcon, "type='signal', interface='com.stericsson.mid.Modem'", &err);
	if(dbus_error_is_set(&err)) {
		if (g_log != NULL)
			fprintf(g_log, "DBUS match error %s: %s\n", err.name, err.message);
		goto error;
	}
	dbus_connection_remove_filter(dbcon, (DBusHandleMessageFunction)handle_dbus_message, NULL);
	dbus_connection_unref(dbcon);

	return 0;
error:
	if(dbus_error_is_set(&err))
		dbus_error_free(&err);
	if (g_log != NULL)
		fclose(g_log);

	return 1;
}
